package com.example.springsearchengines.service;

import com.example.springsearchengines.config.PathConfig;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 通过这个类构造出索引结构
 */
public class Index {

    //索引硬盘存储位置
    private static String INDEX_PATH;

    static {
        if(!PathConfig.isOnline) {
            INDEX_PATH = "D:/doc_searcher_index/";
        }else {
            INDEX_PATH = "/root/spring-search-enigne/doc_searcher_index/";
        }
    }

    //使用当前对象来进行序列化和反序列化
    private ObjectMapper objectMapper = new ObjectMapper();

    //这里的两个锁对象是针对构造正排和倒排索引时所可能出现的线程安全问题所设置的锁
    //由于操作的是不同的数据结构，所以使用两个对象
    private final Object object1 = new Object();
    private final Object object2 = new Object();

    //使用数组下标表示docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    //使用hash表来表示倒排索引
    //key就是词，value就是关联该词的一组文章
    private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();

    //1.给定一个docId，在正排索引中，查询详细的文档信息
    public DocInfo getOocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    //2.给定一个词，在倒排索引中查询出哪些文档与它相关联
    public List<Weight> getInverted(String word) {
        return invertedIndex.get(word);
    }

    //3.往索引中中新增一个文档
    public void addDoc(String title,String url,String content) {
        DocInfo docInfo = new DocInfo();
        //构造正排索引
        docInfo = buildForward(title,url,content);
        //构造倒排索引
        buildInverted(docInfo);
    }

    private DocInfo buildForward(String title,String url,String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setUrl(url);
        docInfo.setTitle(title);
        docInfo.setContent(content);
        synchronized (object1) {
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }


    private void buildInverted(DocInfo docInfo) {
        class WordCnt {
            public int titleCount;
            public int contentCount;
        }
        synchronized (object2) {
            //当前map用来统计分词结果
            HashMap<String,WordCnt> wordCntHashMap = new HashMap<>();
            //1.针对文档标题进行分词
            //已经对大写进行转换成小写
            //仔细观察，HEllo和hello搜索出来的结果一样，所以遇到这两个单词，应该记两次
            List<Term> titleTerms = ToAnalysis.parse(docInfo.getTitle()).getTerms();

            //2.遍历分词结果，统计出每个单词出现的次数
            for(Term term : titleTerms) {
                String word = term.getName();
                WordCnt wordCnt = wordCntHashMap.get(word);
                if(wordCnt == null) {
                    WordCnt newWordCnt = new WordCnt();
                    newWordCnt.titleCount = 1;
                    wordCntHashMap.put(word,newWordCnt);
                }else {
                    wordCnt.titleCount += 1;
                    wordCntHashMap.put(word,wordCnt);
                }
            }

            //3.针对正文页进行分词
            List<Term> contentTerms = ToAnalysis.parse(docInfo.getContent()).getTerms();
            //4.遍历分词结果，统计每个词出现的次数
            for(Term term : contentTerms) {
                String word = term.getName();
                WordCnt wordCnt = wordCntHashMap.get(word);
                if(wordCnt == null) {
                    WordCnt newWordCnt = new WordCnt();
                    newWordCnt.contentCount = 1;
                    wordCntHashMap.put(word,newWordCnt);
                }else {
                    wordCnt.contentCount += 1;
                    wordCntHashMap.put(word,wordCnt);
                }
            }

            //5.把上面的结果汇总到一个hashMap中，最终文档的权重，就设定成标题中出现的次数 * 10 + 正文中出现的次数
            //6.遍历刚才的hashMap，以此来更新倒排索引中的结构了
            for(Map.Entry<String,WordCnt> entry : wordCntHashMap.entrySet()) {
                //先根据这里的词去倒排索引中查一查
                ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());
                if(invertedList == null) {
                    //表示倒排索引中没有相关词的文档
                    //要进行构造
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    //权重为标题中出现的次数*10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount*10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(), newInvertedList);
                }else {
                    //如果不为空，就把当前这个文档构造出一个weight对象，然后添加到已有的倒排队列中
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount*10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }
        }
    }

    //4.把内存中的索引结构保存在硬盘中
    public void save() {
        //首先判断当前文目录是否存在，若不存在则进行创建
        System.out.println("保存索引开始");
        long start = System.currentTimeMillis();
        File indexPathFile = new File(INDEX_PATH);
        if(!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        try {
            File forwardFile = new File(INDEX_PATH + "forward.txt");
            File invertedFile = new File(INDEX_PATH + "inverted.txt");
            objectMapper.writeValue(forwardFile,forwardIndex);
            objectMapper.writeValue(invertedFile,invertedIndex);
        }catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成，加载时间: " + (end-start) + "ms");
    }

    //5.把硬盘中的索引加载到内存中
    public void load() {
        System.out.println("加载索引开始");
        long start = System.currentTimeMillis();
        File forwardFile = new File(INDEX_PATH + "forward.txt");
        File invertedFile = new File(INDEX_PATH + "inverted.txt");
        try {
            forwardIndex = objectMapper.readValue(forwardFile, new TypeReference<ArrayList<DocInfo>>(){});
            invertedIndex = objectMapper.readValue(invertedFile, new TypeReference<HashMap<String,ArrayList<Weight>>>(){});
        }catch (IOException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("加载索引完成，加载时间: " + (end-start) + "ms");

    }
}
